동현의 나불나불
백엔드
배포
docker, AWS, github action을 활용한 배포
github_actions_기초
prometheus_이론
kafka 공부
ch1.kafka 시작하기
언어
자바
java 인터페이스
java다형성
java예외
java추상화
java컬렉션
javaThread
프로젝트
바로그림
django-학습-핵심-정리
elasticsearch-검색과-데이터-파이프라인-정리
활용한 기술
바로그림-프로젝트-설계와-시행착오
회고록
백엔드_통합점검
오늘의 식사
Today I Learned
2026_04_15~2026_04_19
2026_04_15
2026_04_16
2026_04_17
2026_04_20~2026_04_27
2026_04_20
2026_04_21
2026_04_22
2026_04_23
2026_04_24
2026_04_27~2026_05_01
2026_04_27
2026_04_28
2026_04_29
2026_04_30
2026_05_01
2026_05_04~2026_05_08
2026_05_04
2026_05_05
2026_05_06
2026_05_07
2026_05_08
2026_05_11~2026_05_15
2026_05_11
2026_05_12
2026_05_13
2026_05_14
2026_05_15
2026_05_21~2026_05_22
2026_05_21
2026_05_22
2026_05_27~2026_05_30
2026_05_27
2026_06_TIL
2026_06_11
2026_06_15
2026_06_16
2026_06_22
2026_06_25
2026_06_26
2026_06_28
2026_07_TIL
cs
네트워크
네트워킹 하향식 접근
http 프로토콜
데이터베이스
DB_SQL기초
Lock_diffrent
웹
os
chapter1 운영체제 서론
chapter2 운영체제 구조
chapter3 프로세스 관리
Home
Contact
Copyright © 2024 |
Yankos
Home
>
Today I Learned
> 2026_04_20~2026_04_27
Now Loading ...
2026_04_20~2026_04_27
2026-04-24 정리본
보충 1차 캐시는 identity 즉, 동일성을 보장한다. 같은 트랜잭션(persistence context)내에서는 동일한 엔티티를 조회할 경우 항상 같은 객체 참조를 반환하여 == 비교가 성립한다. 이 보장은 트랜잭션 내부에서만 유효하기 때문에 다른 트랜잭션에서는 별도의 persistence context를 사용해서 == 비교는 성립하지 않는다. 그리고 트랜잭션의 격리수준 Read Committed은 1차 캐시가 아닌 DB가 보장하는 것이고 이게 잘 되어있어야지 2차 캐시의 역할을 수행하는 Redis까지 사용하는 것이 가능하다. 여기서 Redis가 수행하는 것은 DB의 접근을 줄이고 분산환경에서 캐시를 공유하는 역할을 수행한다. 정규화 데이터베이스을 사용하고 추구하는 것 중 하나로는 데이터베이스를 중복없이 일관성 있게 유지하는 것이다. 우리가 테이블을 만들 때 여러가지 정보를 넣는 경우 필요한 정보지만 다른 attribute의 요소의 삭제 때문에 없앨 수가 있다. 이러한 사고를 방지하고 데이터의 무결성 유지를 위해 테이블을 분해하는 과정을 정규화라고 한다. 디민 정규화를 많이 하는 것이 성능과 직결되는 것이 아니다. 테이블을 join연산을 시킬 수록 더 많은 연산이 필요하기 때문에 적절하게 쓰는 것이 좋다. 그래서 실무에서는 대부분 3NF까지 혹은 경우에 따라 BCNF까지 사용한다. 그리고 쿼리의 속도를 높이기 위해 테이블을 합치는 경우가 있는데 이는 반정규화라고 한다. 함수종속 어떤 컬럼의 값이 다른 컬럼의 값을 결정하는 단계이다. 즉 어떤 값이 주어졌을 때 그 컬럼과 함수 종속관게에 있는 다른 컬럼의 값이 정해지는 것을 의미한다. A->B로 표기 정규화 진행시 기준이 되는 개념이다. 즉 데이터 중복을 줄이고 이상현상을 방지하기 위해 분해할 때의 기준이다. 이상현상 삽입 이상(Insertion Anomaly) 원하지 않는 데이터까지 같이 넣어야만 삽입이 가능한 문제 갱신 이상(Insertion Anomaly) 하나의 데이터를 수정할 때 여러 곳을 다 바꾸어야 하는 문제 삭제 이상(Deletion Anomaly) 특정 데이터를 삭제 했는데 원래 보존해야 할 정보까지 같이 삭제되는 문제 1NF(원자성 확보) 태이블의 모든 속성이 원자값을 가지게 하는 경우 하나의 컬럼에는 하나의 값만 가져야 한다. -> 컬럼 개수에 의존하는 유연성이 적어진 형태가 된다. 2NF(부분종속 제거) 기본키가 학생ID가 학생명을 과목 명 없이 결정할 줄 알아야 한다. 학생ID랑 과목코드가 함수종속이 된다. 성적이 추가된다면 함수종속이 된다. 종속이 기본키 전체가 아니라 일부 키에 의존할 경우 발생하는 부분함수 종속을 제거해야 한다. 부분함수 종속이 된다. 부분함수 종속을 제거하면 어떤 속성이 무엇에 의해 결정되는지 분류하는 것이 가능하다. 3NF(이행함수 제거) 2정규화를 만족하는 테이블 중 이행함수 종속을 없애는 것이다. A→B, B→C 이면 A→C에 대해 함수종속이 이루어지는 경우 이러한 간접적인 종속을 제거하는 것이다. 학생 ID가 있고 학과 코드가 있다고 하면 학과 코드가 학과장한테 되어있으면 학생ID가 종속관계를 보고있기 때문에 개선할 필요가 있다. BCNF(유일성 만족하지 않는 컬럼에도 종속 제거) 모든 결정자가 반드시 후보키여야 한다. X→ Y, X는 반드시 후보키여야 한다. 일반적으로는 3NF까지 정규화를 진행하면 대부분의 이상현상을 제거할 수 있기 때문에 실무에서도 충분한 경우가 많다. 다만 특정 테이블에서 후보키가 아닌 속성이 다른 속성을 결정하는 경우에는 BCNF를 적용하여 추가적인 이상현상을 제거할 수 있다. 그러나 BCNF까지 정규화를 진행하면 테이블이 더 분해되어 조인이 증가하게 되므로, 조회 성능이 저하될 수 있어 상황에 따라 선택적으로 적용한다. 동시성 제어 - 고립과 잠금 (매우 중요, 면접 단골) 여러가지 트랜잭션이 가능한 무결성과 정확도를 보장하기 위해 사용하는 기술이다. 동시성에도 이상현상이 발생가능한데 이를 해결하기 위해서 동시성 제어를 위한 고립이 뭔지 잠금이 뭔지에 대해 알아볼 예정이다. 트랜잭선의 속성중 ACID의 I인 isolation은 완전히 보장하려면 트랜잭션 간 간섭을 철저히 차단해야 한다. 하지만 이 경우 동시에 처리할 수 있는 작업량(Throughput)이 크게 감소한다는 단점이 있다. 또한 각각의 트랜잭션은 마치 혼자 실행되는 것처럼 보장할 수 있어야 한다. 그래서 적절한 고립레벨 설정을 하는 것이 중요하다고 볼 수가 있다. 팁 내가 만약 동시성 제어를 한 것을 보여주고 싶으면 각 과정에 따라 속도와 시간을 스냅샷을 찍어두고 성능 개선을 한 상태에서 찍은 스냅샷을 해서 비교한 모습을 보여주도록 하자. 고려해야 하는 Anomaly DirtyRead 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽는 현상 트랜잭션 T1과 T2가 있다고 하면 T1이 진행하고 있는 도중에 T2가 읽었기 때문에 데이터 정합성이 보장되지 않는다. Non-Repeatable Read 한 트랜잭션에서 같은 데이터를 두 번 읽었는데 값이 달라지는 현상 이는 트랜잭션 A는 이체처리 트랜잭션 B는 감사 작업을 수행하는 경우 transactionA, transactionB가 다시 1번을 조회하는 경우 작업이 끝나지 않았는데 동일한 작업에서 값이 다르게 나온 경우 읽기 시점마다 최신 commit된 상태를 읽어서 발생한 문제라고 볼 수가 있다. Phantom Read 같은 조건으로 조회했는데 행(row) 자체가 달라지는 현상, 같은 조건으로 조회했는데 행(row) 자체가 달라지는 현상 범위를 조회했을 때 달라질 수가 있다. SQL 표준 고립 4단계 고립 수준이 높아질 수록 더 많은 이상현상을 대쳐할 수가 있지만 성능이 떨어질 수가 있다. 구현하고자 하는 기능에 맞게 고립 수준을 정하는 것이 좋다. 그리고 참고로 MySQL이랑 MariaDB는 데이터베이스 안에 돌아가는 엔진이 있다. INNODB라는 엔진을 가진다. 이 INNODB는 MVCC패턴이란 것을 적용하게 된다. Read-uncommitted level 커밋되지 않은 레벨까지 읽는다고 보면 된다. 가장 성능이 좋다. 고립레벨이 가장 많이 풀려있음, 3가지 이상현상 모두 가능한 레벨이다. Read Committed 커밋된 데이터만 읽도록 제한힌다. 이 경우 Dirty Read 방지하고 커밋된 데이터만 읽을 수 있도록 방지한다. 다만, 동일한 쿼리를 반복 실행했을 때 non repeatable read와 Phantom Read이 발현될 수가 있다. Dirty Read를 방지한다. 최신 커밋데이터를 읽고 lock이 과도하게 걸리는 상황은 피하려고 한다. repeatable read 트랜잭션의 시작시점을 스냅샷으로 만든다. 동일한 환경 내에서 동일한 결과를 보장하는 것이다. 참고로 MySQL의 transaction level의 default 설정이다. 이 경우 Phantom Read가 나올 수가 있지만 MySQL에서는 MVCC기능과 Next-key-lock으로 phandotm read를 막는다. Next-key-lock: 기존 row와 사이 공간까지 잠금을 걸어 다른 트랜잭션이 그 범위에 insert를 못하게 한다. Serialize 가장 강한 고립레벨, 트랜잭션을 순차적으로 실행한 것과 동일하게 진행, 그래서 가장 성능이 낮다. 트랜잭션간 간섭을 원천적으로 차단, race condition 증가한다. 병목현상이 발생할 가능성이 높아진다. 성능은 안좋지만 은행같이 중요한 데이터를 다루는 곳에서 일부구간에 한정해서 serializable을 사용한다고 한다 MVCC multi version Concurrency Control — 데이터의 여러버전을 유지하여 트랜잭션간 충돌 최소화하는 동시성 제어방식이다. 잠금 없이 일관된 읽기를 가능하게 한다. 데이터를 덮어쓰지 않고 버전을 만들어서 변경 이력 유지하고 트랜잭션마다 다른 시점의 데이터를 보게하는 방식이다. 이때 버전을 유지하기 위해 버전을 따로 저장하는 컬럼이나 updatedAt을 만들어서 시간을 통해 결정하도록 한다. Lock 잠금 데이터에 접근하는 권한을 제한하고 트랜잭션에 접근하는 권한을 부여하여 읽고 쓰는 것을 제한하는 기법이다. 이 기법을 통해 동시성 관리, 데이터 정합성 보장, 자원 관리를 할 수가 있다. 잠금의 작동방식 어디에서, 어떤 전략으로 이루어지느냐에 따라 여러 종류로 나뉘게 된다. JVM, OS, DB등 의미가 조금씩 다르지만 핵심원리는 동일하다고 볼 수가 있다. 공유잠금 shared Lock 읽기 잠금(Read Lock)이라고도 하고 여러 개의 트랜잭션이 동시에 같은 데이터에 공유 잠금 획득가능하다. 만약 공유잠금이 존재하면 배타 잠금 불허한다. 공유 잠금 해제까지 다른 트랜잭션은 기다려야 한다. 잠금이라는 것은 말 그대로 읽기 작업을 수행할 때까지 잠금한다는 것을 의미한다. 배타잠금 Exclusive Lock 쓰기잠금(write lock)이라고도 한다. 트랜잭션이 잠금을 획득하면 다른 모든 접근을 차단한다. 수정중인 데이터가 외부로 노출되면 안되기 때문에 원자성으로 이루어지지 않는다. 왜냐하면 트랜잭션 중간에 다른 트랜잭션이 읽으면 불안정한 상태가 노출되기 때문이다. 잠금의 전략, 잠금의 철학적 방식? 동시성을 바라보는 관점에서 잠금을 적용하는 전략이다. 사용하는 목적과 현실적인 상충방식을 고려하고 적용하게 된다. 낙관적 락 트랜잭션에서 데이터 충돌이 자주 발생하지 않을 것이라고 생각하고 커밋직전에 어떤 충돌이 있었는지 살펴본다. 만약 다른 트랜잭션이 자기를 바라봤으면 취소하고 다시시도한다. 선조치 후보고 같은 느낌으로 수행을 하게 된다. 우선 시도 commit하고 만약 충돌이 일어났다면 재시도 하던지 보고나서 다시해 이런 방식 읽기 작업이 많고 충돌이 드문 환경에서 높은 동시성을 제공한다. 재시도 비용이 크기 때문에 많은 충돌이 있는 환경은 피하는 게 좋다 updatedAt 타임스태프나 버전 어노테이션을 사용하여 동시성 제어를 한다. 낙관적 락을 이것을 사용하여 적용 낙관적 락을 걸면 커밋하는 시점에서 검사하기 때문에 이것을 표현하려는 수단을 만들어두면 어플리케이션에서 조절이 가능하다. 다만 정합성에 관한 문제가 발생할 수가 있다. 비관적 락 데이터 충돌이 언제든지 발생가능하다고 가정하고 미리 잠금을 거는 방식 데이터 정합성이 절대적으로 보장되어야 하는 상황에서 선택한다. 비관적 락은 충돌이 될 확률이 매우 큰 경우에 시행한다. 이때 잠금을 보유한 트랜잭션 작업이 길어지면 Deadlock 발생 가능성이 높아진다. 티켓 같은 데이터를 다룰 때 이 비관적 락을 사용하는 것이 조금 더 좋다고 볼 수가 있다. 잠금을 보유한 트랜잭션이 오래걸리면 교착상태에 걸릴 수가 있다. 예를 들어 티켓팅 시스템과 같은 경우 비관적 락과 메세징 큐를 활용하여 순차적 큐를 보장해준다. 이러한 메세징 큐를 제공해주는 프로그램 중에는 레비뎀 큐(오픈소스), kafka가 있는데 이를 메세징 브로커라고 한다. 이러한 프로그램을 다른 말로 MQ라고 한다. 참고 멱등성 : 몇번을 해도 같은 결과가 나오게 하는 것 채팅기능 프로그램을 구현할 때 레비뎀 큐, 카프카를 같이 써보는 것이 좋다 애플리케이션이 메세지를 교환할때 카프카를 이때 활용하는 편이 좋다 교착 상태, Race condition 서로 다른 두개이상의 트랜잭션이 각각 lock을 보유한 상태로 잠금해제를 대기하는 경우를 의미한다.이경우 외부개입이 없으면 영원히 대기상태에 빠지게 된다. 교착상태 조건 무조건 4가지를 충족해야지 교착상태가 일어난다. 이 4가지 조건을 → 코프만 조건이라고 부른다. 상호배제(Mutual Exclusion): 잠금은 한 번에 하나의 트랜잭션만을 보유한다. 점유와 대기(Hold and Wait): 트랜잭션이 추가적인 잠금을 요청하는 단계 비선점(No preemption):다른 트랜잭션의 잠금을 강제로 획득/회수 불가능 DBMS에서 race condition을 풀기 위해 다른 하나의 트랜잭션을 롤백시켜서 이런 데드락을 푸는 경우가 있다. 순환대기(Circular Wait) : 트랜잭션들이 이 원형의 구조로 서로의 잠금을 대기 교착상태 처리 예방 순서를 만들어 놓는다. 간단한 예방방법으로 잠금 타임아웃이 있다. 먼저가져간 애가 타임아웃을 정하고 일단 그 시간이 지나면 잠금을 풀어버린다. 이경우를 잠금 타임아웃이라고 한다. 회피 이 데드락이 발생할 것 같은 요청을 자체에 사전차단하는 것이다. 은행원 알고리즘이 이 회피에 포함된다. 예를 들어서 시스템이 각각의 트랜잭션이 어떤 작업을 하는지, 자원을 가지고 있는지 본다. 시스템이 허용되는 역량 이상의 자원을 쓰려면 시스템이 막는다. → 실무에서는 안된다. 두 트랜잭션의 자원과 작업을 아는데 너무 많은 비용을 소모하기때문이다. 탐지 후 복구 탐지 후 보고, 한명을 희생하는 방식으로 동작 희생을 할 트랜잭션을 선택하고 강제로 롤백을 시킨다. 테스트 위주의 코드 생성 및 실습 public class JpqlTests { private static final EntityManagerFactory emf = Persistence.createEntityManagerFactory("hibernate-exp-6"); void main(){ setup(); tearDown(); } void setup(){ EntityManager entityManager = emf.createEntityManager(); EntityTransaction tx = entityManager.getTransaction(); try{ tx.begin(); Member m1 = new Member("오지원","pass1","ojiwon@chicken.com"); Member m2 = new Member("배라자","pass2","beraja@chicken.com"); Member m3 = new Member("문현민","pass3","moonhm@chicken.com"); entityManager.persist(m1); entityManager.persist(m2); entityManager.persist(m3); tx.commit(); } catch (Exception e) { tx.rollback(); throw new RuntimeException(e); }finally { entityManager.close(); } } void tearDown(){ emf.close(); } } setup()과 tearDown()으로 테스트 코드를 분리하여 테스트를 진행했다. 위 두개의 테스트는 서로에게 영향을 주지 않도록 설계를 하고 진행했다. 이렇게 하면 테스트는 항상 같은 결과를 내야 함에 만족하기 때문에 좋은 테스트라고 볼 수가 있다. JPQL 특정 데이터베이스에 종속되지 않는 객체지향 쿼리 언어이다.SQL과 유사하지만 테이블 대신 entity이름으로 쿼리 작성한다. 내부적으로는 SQL로 변환된다. String jpql = """ select m from Member m where m.orders.is not empty """; 테이블이 아닌 엔티티 객체를 대상으로 실행을 한다. 다만 where문을 사용할 때 n+1과 같은 문제를 조심해야 한다. 이 경우는 fetch join을 사용하여 어느정도 해결 할 수가 있다. SELECT m FROM Member m JOIN FETCH m.orders
Today I Learned
· 2026-04-24
2026-04-23 정리본
부트스트래핑 bootstraping 애플리케이션이 실행될 떄 필요한 객체들을 초기화하고 준비하는 과정이다. 이 과정에서 데이터 상호작용을 추상화를 위해 ORM기술을 도입했다. 컨테이너 방식의 부트스트래핑 WAS가 실행될 때 초기화하고 WAR배포를 기반으로 두고 실행한다. 이때 서버가 생명주기를 관리하고 설정은 주로 XML이나 Spring에서 설정을 한다. EntityManager EntityManager은 jpa가 제공하는 기능으로 내부에는 Persistence Context, 1차캐시, 변경 감지 상태, 트랜잭션 hibernate는 이에 대한 보충으로 생명주기를 포함한 메서드를 제공해준다. jpa는 하나의 작업당 하나의 entityManager을 사용하게 된다. EntityManager와 ThreadSafe Entitymanager은 상태(state)를 가지 객체이기 때문에 threadSafe하지는 않다. 만약 이 상황에서 멀티쓰레드로 사용한다면 위험할 수가 있다. 그래서 요청 단위로 생성해서 사용하는 것이 좋다. Spring framework에서는 tomcat webserver을 사용하여 하나의 요청당 하나의 쓰레드를 생성하기 때문에 entityManager를 바인딩하여 처리를 하게 된다. Singleton 구조인 EntityManagerFactory를 만들어서 시행하기 EntityManagerFactory라는 직접 비지니스 작업을 수행하지 않고 persistence unit정보, DB 연결 설정, 매핑 메타데이터 등의 공유해도 되는 정보를 갖고 있는 객체를 만들고 각 쓰레드가 자기만의 EntityManager을 받도록 하여 처리를 한다. 참고 singleton으로 두어야 설정 - 무거운 초기화 비용이 크다. - 설정 정보를 중심으로 가진다. - 여러 요청이 공유해도 안전헤야 한다 public class App { private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("myUnit"); public void doWork() { EntityManager em = emf.createEntityManager(); try { // 작업 } finally { em.close(); } } } 위 코드와 같이 싱글톤으로 설정한 EntityManageFactory 객체를 두고, 각 쓰레드로 사용할 EntityManager은 EntityManageFactory로 부터 만들어지게 해서 수행을 하였다. 사용한 annotation 정리 @Entity @Table(name = "order_items") public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; } @Entity Entity로 만들기 위해 사용 @Table java의 객체이름과 SQL에서 설계한 테이블의 이름이 다를 수 있기 때문에 sql에서도 이를 인식할 수 있게 어노테이션으로 적어놓는다. @Id Entity의 primary key를 정하는 어노테이션이다. @Enumerated(EnumType.STRING) enum 타입으로 선언한 요소를 사용하고 싶을 때 쓰는 어노테이션이다. @GeneratedValue(strategy = ?) 엔터티의 기본 키를 어떻게 생성할 지 전략을 지정하는 어노테이션 전략 종류 strategy = GenerationType.IDENTITY DB가 PK를 자동생성, JPA가 아닌 DB가 키를 만드는 방법으로 가장 많이 사용하는 방법이다. strategy = GenerationType.SEQUENCE DB의 SEQUENCE를 이용한다. DB 내용물의 해쉬처럼 순서를 매겨서 이를 가져와서 사용한다.이를 사용한다면 INSERT전에 jPA가 id를 먼저 확보하고 진행하여 성능은 되게 좋다고 볼 수가 있다. 쓰기 지연(Transactional Write-Behind)을 사용하는 것이 가능하다. strategy = GenerationType.TABLE 별도의 테이블로 id를 관리한다. 하지만 동시성 문제가 많이 발생할 우려가 있어서 실무에서는 거의 쓰지 않는다. strategy = GenerationType.AUTO DB종류를 보고 알아서 결정한다 편하지만 예측하기 어렵다. 별도의 strategy를 설정하지 않는다면 default 설정은 auto로 동작한다. @Column() 테이블의 컬럼에 관한 설정하는 어노테이션이다. 제약조건 같은 것을 덧붙일 수가 있다. (nullable = false) null값을 방지하는 제약을 건다 (columnDefinition = “INT DEFAULT 0”) Default 값을 0으로 설정한다. @ManyToMany, @OneToMany, @ManyToOne 다른 테이블과의 관계 설정 @JoinTable @JoinTable( name = “product_categories”, joinColumns = @JoinColumn(name = “product_id”), inverseJoinColumns = @JoinColumn(name = “category_id”) ) product와 category의 관계를 product_categories라는 중간테이블로 연결 joinColumns = @JoinColumn(name = “product_id”)은 현재 엔티티의 FK컬럼이고 자기 자신의 PK를 저장하는 칼럼을 의미한다. inverseJoinColumns은 연결대상의 엔티티의 PK를 저장하게 된다. 자기순환 문제 자기참조는 category와 같이 부모자식 형태를구현할 수 있다는 장점이 있지만 이게 양방향 관계에서 나타내게 된다면 문제가 될 수가 있다. 양방향 관계에서 이게 나타나기가 쉽고 실행시켰을 때 무한 재귀가 발생할 수가 있다. 또한 영속성 컨테스트에서 데이터 불일치까지 나올 수가 있다. 단방향 관계와 양방향 관계 단방향 관계는 한쪽 엔티티만 다른 엔티티를 참조하는 구조 양방향 관계는 서로 참조하는 구조이다. 단방향 관계에서 외래키를 가진 쪽이 주인이 되어 @ManyToOne을 설정한 column이 있는 쪽이 주인이 된다. 양방향은 상태 불일치, 순환 참조문제, 관리 포인트 증가와 같은 문제로 사용하는 것이 권장되지 않는다. 만약 양방향을 사용하고 싶으면 편의 메서드를 반드시 작성해야 한다. 편의 메서드 연관 관계를 한 번에 올바르게 설정해주는 메서드이다. 양방향은 설정하면 안되지만 한쪽을 작업하면 다른 한쪽도 반영할 필요가 있기 때문에 사용한다. 일관성 유지를 위해 사용한다. public void addOrderItem(OrderItem item) { items.add(item); item.setOrder(this); } 참고: entity 객체는 final을 쓰지 말것 편의 메서드를 사용하여 일대일 관계에서 관계의 주도권이 address 클래스가 가지고 있는데 member 클래스에서 setMember을 호출하여 정보를 가져올 수가 있다. 이 경우 순환구조가 생길 우려가 있어서 조심해야 하는데 이때 메서드를 protected를 사용해 연관관계 편의 메서드에 접근제한을 하여 외부 클래스에서의 접근을 막는다. CRUD 적용 실습 private static void updateMember(Long id, String newName) { EntityManager entityManager = emf.createEntityManager(); EntityTransaction tx = entityManager.getTransaction(); try{ tx.begin(); //select * from member where id = -99999; //resultset = `````` Member findMember = entityManager.find(Member.class, id); if(findMember != null){ findMember.setName(newName); } //안써줘도 상관없음 변경감지를 하기 때문에 entityManager.persist(findMember); tx.commit(); } catch (Exception e) { e.printStackTrace(); tx.rollback(); }finally { entityManager.close(); } } 이때 사용하는 것은 update작업이라서 entityManager에서 변경감지를 하여 굳이 .persist 메서드를 사용할 필요가 없는 것이다. 참고 : java bin 방식으로도 xml 파일을 초기화 하는 것이 가능하다. 팁 : 개발 시 마크다운에 기능별 to do list 같은 것을 만들어 체크하면서 진행하면 좋다. 참고 : MyBatis의 2차캐시는 앵간하면 끄는 것이 맞다. 데이터 정합성 문제, 캐시 무효화 타이밍 어려움, 멀티 서버 환경에서 위험이라는 문제가 있어서 기본적으로 끄는 것이 좋다.
Today I Learned
· 2026-04-23
2026-04-22 정리본
Mybatis 보충 mybatis를 사용하면서 개인적으로 놓친 부분이라서 다시 공부를 해야겠다. resultType과 resultMap mybatis는 xml -> namespace(인터페이스) 확인-> SQL 내부 저장 -> getMapper()호출 -> 인터페이스 기반의 프록시 생성 -> 메서드 호출시 SQL 실행이라는 과정을 거치게 되는데 xml파일에서 쿼리를 작성하고 이에 관한 값을 java 파일에서 사용을 할지 아니면 xml파일 내 다른 구문에 전달을 할 지를 결정하게 된다. 이때 자바객체로 반환을 할 경우 resultType, xml에서 지정한 파일로 넘기고 싶을때는 resultMap을 사용하여 넘기게 된다. 이때의 resultMap은 java가 xml에서 작업한 join 쿼리와 같은 결과물들을 인식하도록 하는 설계도가 된다. 예제코드 <resultMap id="memberDtoResultMap" type="domain.MemberDto"> <id property="memberId" column="member_id" /> <result property="memberName" column="member_name" /> <result property="memberBalance" column="member_balance" /> </resultMap> <select id="findAll" resultMap="memberDtoResultMap"> SELECT id AS member_id, name AS member_name, balance AS member_balance FROM member </select> 위 코드는 interface로 만든 MemberDTO 타입의 결과를 전송하기 위해 매핑한 resultMap과 member정보를 DTO로 반환할 select 쿼리를 가져온 것이다. <select id="findById" parameterType="long" resultType="domain.Member"> SELECT id, name, email, balance FROM member WHERE id = #{id} </select> 여기서 resultType에 domain 패키지의 Member로 타입을 변경하여 반환하는 것이다. 그리고 여기서 parameterType은 findById에 어떤 타입의 파라미터가 들어왔을 때 시행할 것인지를 명세화한 것이다. Mybatis에서의 cache mybatis에서 성능 최적화를 위한 기능이다. 같은 SQL 결과를 내부 메모리에 저장해두고 재사용을 해서 DB조회를 줄이는 기능이다. 실제로 실행시켜 보면 캐시를 사용한 경우 SQL문을 읽지 않고 동일한 결과가 나옴을 알 수가 있다. DB와 상호작용하는 작업 단위 객체인 Session를 단위로 하는 1차 캐시(local cache) mapper 단위의 2차캐시(second level cache)가 존재한다. 이 cache는 commit(), rollback(), close(), INSERT, UPDATE, DELETE, 데이터 변경과 같은 경우 초기화를 하게 된다. Mybatis에서의 1차캐시(Local cache) 1차 캐시는 같은 세션 내에서 동일한 쿼리를 재사용하는 기능이다. 여기서 사용하는 세션은 SqlSession으로 같은 쿼리 + 같은 파라미터를 가지는 경우 캐시가 이루어진다. 하지만 같은 세션 내 다른 객체로 설정해 놓은 Member m1, Member m2를 파라미터 1로 조회하는 경우 캐시가 같은 객체로 취급을 하게된다. 이 때문에 여러 트랜잭션이 존재하는 큰 시스템에서 데이터 정합성 때문에 사용을 조심해야 하고 캐시에 의존하지 말고 DB기준으로 생각해야 한다. Mybatis에서의 2차 캐시(Second level cache) 2차 캐시는 Mapper 단위로 여러 SqlSession 간 공유되는 캐시이다. 세션단위로 불러오는 결과가 commit되는 경우 이를 2차 캐시에 저장하여 활용할 수가 있다. 2차 캐시에 객체를 저장하려면 java의 Serializable 인터페이스를 사용해 직렬화를 할 필요가 있다. 직렬화를 통해 객체를 바이트화를 하고 바이트화 된 값을 객체로 바꾸는 역직렬화 과정을 거쳐 캐시에서 빼내 반환을 하게 된다. 만약 readOnly 모드로만 사용한다면 직렬화 과정을 거칠 필요는 없다. 직렬화를 하는 이유는 <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/> 여기서 eviction은 cache를 관리하는 방법을 설정하는 것이고 FIFO, LRU 등 os의 메모리를 관리해주는 방법을 가져왔다. 흔히 LRU로 사용한다고 한다. flushInterval은 캐시가 초기화되는 시간을 설정하는 것이다. 정해진 시간이 지나면 캐시는 초기화된다. size는 캐시의 크기를 의미하고 readOnly는 읽기전용인지 아닌지를 설정한다. 만약 읽기 전용으로 설정할 시 성능이 올라간다. Cache Hit Ratio [mapper.s5.ProductCacheMapper]: 0.5 전체 조회중 절반은 캐시에서 가져왔고 절반은 DB에서 가져왔다는 의미 , Hit ratio = 캐시 적중 횟수/ 전체 조회 횟수를 의미하고 이 cache Hit Ratio를 통해 캐시를 얼마나 효율적으로 사용하는지 볼 수가 있다. sql join 복습 여러 테이블이 존재할 때 테이블간의 join이 이루어지는 과정 <select id = "findAllOrder" resultMap="orderFullMap"> SELECT o.id AS order_id, o.member, o.ordered_at, o.status, m.id AS m_id, m.name AS m_name, m.email AS m_email, m.balance AS m_balance, oi.id AS oi_id, oi.order_id AS oi_order_id, oi.product_id AS oi_product_id, oi.quantity AS oi_quantity, oi.price AS oi_price, p.id AS p_id, p.name AS p_name, p.price AS p_price, p.stock AS p_stock FROM orders o JOIN member m ON o.member_id = m.id JOIN order_item oi ON o.id = oi.order_id JOIN product p ON oi.procuct_id = p.id </select> 이렇게 여러 테이블을 가져오는 경우 SQL에서 작성된 순서대로 JOIN이 진행된다. order -> member -> order_item -> product 순으로 진행이 이루어진다. 하지만 sql 옵티마이저에 의해 순서가 바뀔 수가 있는데 확인을 해보고 싶다면 Explain문을 사용하여 알아보도록 하자. 기준테이블 -> ToOne -> ToMany -> 추가 확장을 거치게 되서 권장되는 순서를 지킨다고 볼 수가 있다. JAVA에서의 ORM 구성 서론 JDBC가 java와 SQL의 연결을 하여 java에서 데이터베이스를 사용할 수 있었지만 시간이 지날 수록 JDBC의 단점 중 하나인 불필요한 반복을 CRUD의 쿼리를 작성하는데 많이 느끼게 되었다. SQL에서는 join으로 해결할 수 있었던 문제를 java에서는 연관관계에 되어 있는 객체를 가져올 방법은 없어서 다른 방법을 사용해주어야 했다. 이를 위해서 sql로 직접 쿼리를 작성해야 하는 것도 문제가 되었다. 자바의 가장 큰 장점은 객체지향인데 sql을 직접 쿼리를 작성해서 사용하면 sql에 의존적인 프로그램을 짤 수 밖에 없다는 것이 이전의 JDBC 구조의 단점이다. 그리고 관계형 데이터베이스에서 새로운 컬럼을 추가하는 경우 그 컬럼에는 null값이 추가된다. 그리고 데이터베이스만 바뀌는 것은 상관없는데 복잡한 프로그램에서는 그 테이블의 데이터를 쓰는 코드를 다 바꾸어야 하는 문제로 확장될 수가 있다. 이러한 차이를 극복하기 위해 사용하는 기술이 ORM이다. ORM(Object Relational Mapping)이란? ORM은 관계형 데이터베이스와 java 객체 사이에서 relation을 매핑해주는 기술이다. 테이블이 완전 객체로 바뀌고 참조의 참조를 하는 것이 가능하다. 그리고 xml 파일이 없기 때문에 쿼리도 ORM으로 만드는 것이 가능하다. 객체 위주로 데이터베이스를 관리하는 것이 가능해진다. 장점 개발자가 SQL에 의존하지 않고 비지니스 로직을 구성하는 것이 가능 단점 SQL이 객체입장만 고려하고 객체에만 의존하여 SQL이 복잡해질 수가 있다 DB가 개판이 될 수 있는 우려가 있다. → 그래서 DB와 객체 구조 모두 고려해야 한다. JPA(Java Persistence API) 영속적으로 제공 ORM은 개념, ORM을 사용하기 위한 표준 인터페이스가 JPA이다. JPA는 자바의 ORM 기술 표준이자 명세이다. 표준 인터페이스로 제공하고 있어서 JPA만 implement를 한다면 java에서 사용가능하고 다른 JPA 기술을 적용하기 쉬워지고 생산성이 올라간다. 테이블의 명세를 hybernate가 읽어내려서 스키마를 대신 생성까지 대신 해주는 것이 가능하다. @Entity class Member { @Id Long id; String name; } 위 코드와 같이 객체로 테이블을 구성하는 것이 가능하다. JPA를 사용하는 이유 xml의 변경시 java 코드를 변경을 해줘야 하기 때문에 entity 객체에 대한 신뢰도를 떨어뜨린다고 볼 수가 있다. 이런 문제를 객체를 수정해주는 것만으로 해결 가능 각 SQL 모델을 사투리처럼 고려해놓아서 JPA를 거치면 동일하게 알아듣게 된다 Hybernate spring framework가 채택한 JPA, 스프링에서 JPA와 통신하는 표준적인 방식 spring jpa는 hybernate를 한번 더 추상화 한 것이다. hybernate까지는 /resources/META-INF에다가 persistence.xml 파일 하나까지는 필요하다. 웹 구현할 때 패키징을 거치게 되는데 jar, war 양식으로 하게 된다.jar로 압축되어 있는데 클래스 파일들이 하나의 디렉터리로 압축된다. resources 밑에는 민감한 파일이 있어서 압축이 되지 않는데 META-INF 파일에는 중요한 파일이 저장되고 압축이 되지 않는다. 스프링에서는 이 presistence.xml 파일을 대신 만들어주고 .xml 문서가 아닌 다른 타입의 문서를 제공한다. <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <!-- JAVA SE => JAVA Standard Enviroment --> <!-- name = id--> <persistence-unit name="hibernate-exp-1hibernate-exp-1"> <properties> <property name ="jakarta.persistence.jdbc.url" value ="jdbc:h2:mem:test-db"/> <property name = "jakarta.persistence.jdbc.user" value ="sa"/> <property name = "jakarta.persistence.jdbc.password" value=""/> <property name="jakarta.persistence.schema-generation.database.action" value="create"/> <property name ="hibernate.show_sql" value="true"/> <property name ="hibernate.format_sql" value="true"/> <property name="hibernate.highlight_sql" value="true"/> </properties> </persistence-unit> </persistence> 위는 사용하게 되는 hybernate의 persistence.xml 파일이다. persistence context 엔티티를 메모리에서 추적하고 관리하는 작업공간이다. persistence context 흔히 영속성 콘텍스트라고 부르기도 한다. 이는 mybatis의 enviroments라고 봐도 무방하고 persistence.xml파일에서 persistence-unit부분의 집합이라고 봐도 된다. 엔티티의 생명주기를 관리하고 변경 감지, 쓰기 지연, 동일성 보장까지 담당하는 핵심구조이다. Persistence context 내의 생명주기 transient(new) 비영속 상태 entity와 관련없이 그냥 떠있는 상태 managed transient 상태가 persistence context에 의해 관리를 받게 된 상태 detached 준 영속상태 영속성 context에 한번 들어갔다 나간 것을 의미 persistence context가 한번 들어와서 알지만 관리를 하지 않는 상태 removed 삭제를 하면 데이터베이스에서 삭제된다. 데이터베이스랑 인스턴스는 다른 상태이다 영속성 Context에서 남아있는 것이 DB에 남아있는 상태이다. 영속성 context에 떠있는 이상 계속 반영된다 1차 캐시 first level cache 만약 같은 EntityManager/session 안에서 같은 PK를 가지는 엔티티를 두번 조회하면 두번째에는 DB보다는 persistence context에 있는 managed 상태의 객체를 재사용한다. 여기서 중요한 것은 동동성이 아닌 동일성을 보장하는 것으로 같은 영속성 컨테스트 안이라면 row를 여러개 만들지 않고 하나만 관리를 해도된다. Drity Checking 변경감지 transient상태에서 managed로 들어올 때 스냅샷을 찍는데 그 시점에서의 entity의 상태를 순결상태라고 한다. 그리고 변화가 된 시점을 오염물이 묻은 시점이라고 하고 그것을 감지하는 것을 Dirty checking이라고 한다. managed에 올라간 entity는 개발자가 update()와 같은 메서드를 직접 호출하지 않아도 필드가 바뀌면 Hibernate가 flush 시점에 변경 내용을 감지해서 적절한 SQL을 생성한다. tx.begin(); Member member = em.find(Member.class, 1L); member.setBalance(10000); // 객체만 수정 tx.commit(); // flush 발생 -> update SQL DB를 직접 수정을 하는 것이 아닌 영속 객체의 상태를 바꾸고 이것을 Hibernate가 DB에 반영한다. transactional write behind, 트랜잭션 쓰기 지연 SQL을 즉시 DB에 보내지 않고 잠깐 모아두었다가 적절한 시점에 한꺼번에 반영하는 방식이다. 즉, 개발자가 엔티티를 저장하거나 수정했다고 해서 그 순간 바로 Insert, Update, Delete SQL이 즉시 시행되는 것이 아니고 변경을 persistence context와 action queue에 반영해두고 나중에 flush 시점에 SQL을 DB에 보내게 된다. 이렇게 설정한 이유는 이러한 쓰기 지연을 통해 성급하게 DB에 반영하지 않고 트랜잭션의 전체흐름을 보고 정리하여 반영하는 것이 가능하다. 그리고 쓰기 지연을 사용하면 SQL 실행시점을 모을 수 있어서 효율적이다. 그리고 batch insert/update와 같은 최적화와 궁합이 좋다. Annotation 자바에 있는 문법, 아무 뜻이 없지만 metadata가 들어가게 된다. yaml 파일 작성 방법 설정 파일로 정말 많이 활용하는 양식 JSON과 문법양식이 비슷해 치환되기도 한다. key - value 기반의 계층형 데이터 표현 방식을 가지고 계층을 들여쓰기로 표현하기도 한다. 리스트 표현 servers: - app1 - app2 - app3 객체 리스트 users: - name: kim age: 20 - name: lee age: 30 multi - line 문자열 text: | hello world 이런 문자열인 경우 hello world로 출력된다. 주의사항 YAML에서는 tab키를 쓰지 말 것 yaml은 space키만을 허용한다 name: “hello” 와 name : hello는 동일한 문자열로 취급된다 문자열과 구분하기 위해서 따옴표 사용해야 한다. value : “yes” ->yes는 boolean으로 해석될 수가 있다.
Today I Learned
· 2026-04-22
2026-04-21 정리본
MyBatis DynamicQuery 구현 MyBatis if문 구현하기 <select id="search" resultType="domain.Product"> SELECT id, name, price, stock FROM product WHERE 1=1 <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test = "minPrice != null and minPrice > 0"> AND price >= #{minPrice}} </if> <if test = "maxPrice != null and maxPrice > 0"> AND price <= #{maxPrice}} </if> 여기서 부등호 >는 앵글브라켓일 수도 있고 부등호가 될 수가 있는데 컴퓨터는 이를 비교해주기가 힘들다. 그래서 크고 작고를 사용할 것이면 대체 식을 사용해야 한다, escaping : 위혐이 될만한 문자열을 다른 문자열로 대체하는 것 왼쪽이 오른쪽보다 큰,작은 부등호 표시 >, < 그럼 && 와 같은 and 연산자와 헷갈릴 수 있기 때문에 이에 관한 escaping 문자가 필요하다 &는 & 부등호와 같이 and 연산자를 표시해주는 기능을 수행한다. MyBatis for문 구현하기 <select id="findByIds" resultType="domain.Product"> SELECT <include refid="productColumns"/> FROM product <where> <choose> <when test="ids != null and ids.size() > 0"> id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </when> <otherwise> 1 = 0 </otherwise> </choose> </where> </select> 여기서 은 MyBatis의 동적 기능을 수행하는 것으로 조건있을 때 AND, OR 정리를 하는 기능이다. 그리고 는 java의 swtich - case와 유사하다고 보면 될 것 같다. 다음과 같은 정렬 기능을 구사하는 경우 enum을 활용해서 하면 좋을 것 같다. <select id="findByIds" resultType="domain.Product"> SELECT <include refid="productColumns"/> FROM product <where> <choose> <when test="ids != null and ids.size() > 0"> id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </when> <otherwise> 1 = 0 </otherwise> </choose> </where> </select> 구현 팁 : 그냥 어노테이션을 활용해 java파일에서 구현하고 구현하기 까다로운 코드를 xml파일에 작성 @Select(""" SELECT id, name, stock FROM product where id = #{id} """) Optional<Product> findById(@Param("id") long id); 간단한 select구문을 사용하는 경우 그냥 xml파일이 아닌 java파일에 어노테이션을 통해 정의를 해도 상관없다. Logback과 Log이야기 Logback이란? java에서 자주 사용하는 로깅 구현체 중 하나이다. 비동기 로깅 지원, 내부 최적화, garbage collector 부담이 적어 대용량 트래픽 서버에서도 사용하기도 한다. 자주쓰이는 Logback.xml 설정파일 <?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <logger name="mapper" level="DEBUG" additivity="false"> <appender-ref ref="STDOUT" /> </logger> <logger name="java.sql.ResultSet" level="DEBUG" /> <root level="INFO"> <appender-ref ref="STDOUT" /> </root> </configuration> Log에 관해서 LOG에 관련된 정보를 절대절대 노출되지 않게 할 것 실제 개발에 있어서 배포를 할 때 로그 레벨을 debug로 낮추어서 배포를 하는 것은 안된다., Info로 급을 올려서 보여지도록 해야 한다 Logback에서의 로그의 분류 ERROR WARN INFO Debug TRACE INFO가 기본으로 설정으로 되어있는 것이고 debug나 trace는 임의로 낮춰야지 볼 수가 있다. WARN은 문제가 될 수 있으나 넘어가도 되는 경우가 있다. 다만 심각한 문제가 될 수가 있으니 살펴보는 것은 좋다. → 논리적으로는 오류이지만 실제로는 오류가 아닌 경우도 있다. ERROR : 오류 발생, spring이 종료 될 수가 있다. FATAL: Error보다 심각한 문제로 실행이 되지 않는다. 참고사항 페이징 myBatis에서 사용하는 페이징은 많은 데이터 중에 일부만 잘라서 조회하는 것을 의미한다. try (SqlSession session = connector.openSession()) { MemberMapper mapper = session.getMapper(MemberMapper.class); List<MemberDto> members = mapper.findAll(); MemberDto findMember = members.getLast(); String memberName = findMember.getMemberName(); System.out.println("memberName = " + memberName); 커서기반 페이징이라는 것이 있는데 이는 OFFSET대신 마지막 값을 기준으로 조회를 하는 것이다. .class 활용 try (SqlSession session = connector.openSession()) { MemberMapper memberMapper = session.getMapper(MemberMapper.class); mybatis가 .class를 보고 MapperProxy로 생성 및 사용하여 어떤 mapper임을 알 수가 있게 하고 메서드의 정보를 분석하는 것이 가능하다. 다른 말로 하면 .class는 이 인터페이스를 기반으로 객체를 만들어줘라는 신호라고 볼 수가 있다. myBatis에서 map 자료구조형을 사용하는 이유 map은 유연한 동적처리를 위해 사용한다. 먼저 테이블의 column이 50개가 넘어가는 데이터의 경우 사용하는 메서드의 파라미터 개수가 너무 많아지고 순서가 헷갈리고 유지보수가 어려워지는 단점이 존재한다. 하지만 mapKey를 변수처럼 활용하면 동적 SQL과 잘맞고 바뀌는 검색 조건에도 유연하게 대응도 가능하다. <select id="search" parameterType="map" resultType="domain.Product"> SELECT id, name, price, stock FROM product WHERE 1=1 <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> DTO (Data Transfer Object) 데이터 전송 객체 데이터를 전송하기 위한 객체를 의미한다. 회원정보의 경우 비밀번호와 같이 개인정보와 직결되는 정보를 보호하고 필요한 데이터 전송을 위해 사용한다. 그리고 데이터 정보를 변경 시 생기는 유지보수 과정을 줄여줌에도 이점을 가지고 있다. 데이터의 Entity가 바뀌어도 그것을 사용하는 API는 깨지지 않는다. controller와 service 사이에서 구현되고 myBatis에서는 DB에서 DTO로 바로 매핑하는 것이 가능하다. 이때 map자료구조형을 활용하여 참고사항 정보를 넘겨줄 때 한 데이터에 관한 컬럼을 전부 넘기는 것보다 목적에 맞는 한개씩만을 넘겨주려고 하는 것이 좋다. 그리고 만약 고객사에서 개인정보를 보내주는 프로그램을 요청할 경우에는 선별을 할 필요가 있다. 그리고 왠만하면 안하는게 좋다. 사용하면 좋은 단축키 원하는 static 메서드를 블록하고 f6번을 누르면 그것을 추출해서 다른 클래스에 생성하는 것이 가능하다. n+1이 나타나는 이유 위 1번 코드를 통해 나오는 결과는 1개이지만 실제 터미널에서 행해진 쿼리를 봤을 때 1 + n번이 발생한 것으로 볼 수가 있다. member을 null로 두지 말고 가져오라고 시켰기 때문에 발생하게 된다. order과 연관된 member을 가져오는데 join 연산을 하지 않아 발생하는 문제라고 볼 수가 있다. 지금은 약 100개의 row에 대해 실행해서 문제가 없었지만 실무에서는 이것보다 훨씬 데이터가 커져 문제가 크게 될 수가 있다. 이러한 n+1문제와 관련되서 lazy loading이 있는데 lazy loading은 중요도가 떨어지거나 당장 화면에 보이지 않는 요소들의 로딩을 우선적으로 시행하지 않음으로서 웹페이지 로딩 기술을 향상시키는 방법이다. 이러한 lazy loading은 프록시 객체를 사용하여 프록시 객체가 실제 값에 접근시 실체화를 하는데 이는 프록시 객체를 여러번 접근할 수 있기 때문에 n+1 문제를 해결하는데 좋은 방법이 아니다. 다른 해결방안으로는 fetch join이 있는데 fetch join은 연관된 엔티티를 join을 해서 한 번의 쿼리로 함께 가져오는 방법이다. 예를 들어 select o from Order o join fetch o.member 위 쿼리처럼 order가 참조하는 member도 같이 채워서 가져오는 방법이다. 이게 왜 n+1문제를 해결하는 방법이 되냐면 fetch join은 연관 객체를 처음부터 같이 조회를 하여 추가 조회를 하지 않아도 된다. 그렇다면 lazy는 안좋은 방법인가? 여전히 lazy는 최적화를 위해 필요한 방법이다. 따라서 기본 설계는 Lazy로 두고 필요한 조회에서 fetch join으로 최적화를 하여 n+1문제를 피하는 방향으로 설계를 두는 편이 좋을 것 같다. 다만 fetch join은 연관객체를 항상 같이 들고오므로 들고오는 데이터 양이 많아질 수가 있다. 그래서 최적화가 필요하고 정확도를 중시하는 분야의 몇몇 기업들은 sql mapper을 사용하여 쿼리 튜닝을 해서 n+1문제 발생을 방지한다.
Today I Learned
· 2026-04-21
2026-04-20 정리본
MyBatis configuration 구성 mybatis의 config파일에서 DOCTYPE configuration으로 선언해주면 configuration, environments, enviroment가 필요하다. <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> MyBatis configuration 각 기능들에 대한 문서 링크정리 configuration properties settings typeAliases typeHandlers objectFactory plugins environments environment transactionManager dataSource Type=POOLED : 커넥션 풀링이 적용되어 있는 데이터베이스가 들어가게 된다. mybatis는 스프링과 다르게 hikariCP가 아닌 자체 커넥션 풀을 사용한다.(성능은 hikariCP가 우세) 실무랑 닿아 있는 커넥션 풀을 구성하고 싶으면 POOLED가 아닌 다른 것을 사용해주어야 한다. property에 값을 부여하게 되면 jdbc에서 만들어주었던 객체를 선언해줌을 알 수가 있다. databaseIdProvider mappers MyBatis-configration 문서의 예시 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="exp1"> <environment id = "exp1"> <transactionManager type = "JDBC" /> <dataSource type = "POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/mybatis_shop" /> <property name="username" value="rapa_mybatis" /> <property name="password" value="pwd1" /> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/TestMapper.xml" /> </mappers> </configuration> dataSource에 커넥션을 어떻게 설정할 지에 대해 적는다. 여기서 제공하는 POOLED는 HikariCP가 아닌 mybatis에서 제공하는 자체 커넥션 풀이다. MyBatis란? MyBatis는 SQL 실행 결과를 객체로 자동 변환해주는 대표적인 SQL Mapper 프레임워크이다. jdbc의 ResultSet 객체로부터 받아오는 값을 객체로 매핑하는 역할을 수행한다. XML에 SQL 쿼리를 직접 작성함에 따라 튜닝할 수 있고 JDBC 코드의 불필요한 반복을 제거할 수 있는 장점이 있다. factory를 활용하여 session 구현 SqlSessionFactory는 MyBatis 전체 설정 정보와 실행환경을 내부에 포함하고 있는 설정 컨테이너 객체이다. 참고로 SqlSession은 쓰레드세이프를 지원하지 않는다. 이 경우 병렬작업을 시도하는 경우 오류 발생할 가능성이 높다. 그래서 한 쓰레드에 하나의 SqlSession을 사용하도록 하는 것이 좋다. spring의 경우에는 tomcat을 활용하면 요청 하나당 하나의 쓰레드를 생성해서 위 문제와는 무관하다고 볼 수 있다. 하지만 쓰레드를 이벤트마다 생성해주는 경우 예외처리를 잘 작성해야 할 것이다. public class SessionFactory1 { static void main(String[] args) throws Exception{ //sqlSessionFactory //DB connection Settings //실제 쿼리를 실행한다. String xmlFileDir = "mybatis-config1.xml"; InputStream xmlInputStream = Resources.getResourceAsStream(xmlFileDir); //MyBatis 전체 설정 정보와 실행환경을 내부에 포함하고 있는 설정 컨테이너 객체 //SqlSession 객체를 생성하는 객체 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xmlInputStream); //실제 작업자 //SqlSession을 하면서 preparedStatement까지 포함한다 try(SqlSession sqlSession = sqlSessionFactory.openSession()){ DataSource dataSource = sqlSessionFactory.getConfiguration() .getEnvironment() .getDataSource(); System.out.println("dataSource" + dataSource.getClass().getName()); System.out.println("Sql 세션 획득 성공!"); //namespace.query_id Integer result = sqlSession.selectOne("test.test"); System.out.println("result : "+ result); }catch (Exception e){ throw new RuntimeException(e); } } hikariCP로 설정을 변경하고 싶은 경우 <dataSource type = "config.HikariCpDataSourceFactory"> <property name="driver" value="com.mysql.cj.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/mybatis_shop" /> <property name="username" value="rapa_mybatis" /> <property name="password" value="pwd1" /> </dataSource> dataSource부분을 설정한 임의의 config파일에 설정한 hikariCP에 저장한 값을 가져올 수 있도록 설정 public class HikariCpDataSourceFactory implements DataSourceFactory { private Properties properties; @Override public void setProperties(Properties props) { this.properties = props; } @Override public DataSource getDataSource() { HikariConfig config = new HikariConfig(); config.setDriverClassName(properties.getProperty("driver")); config.setJdbcUrl(properties.getProperty("url")); config.setUsername(properties.getProperty("username")); config.setPassword(properties.getProperty("password")); return new HikariDataSource(config); } } mapper을 활용한 SQL문 등록하기 mapper 파일에 원하는 sql문을 작성하여 이것을 가져와 사용하는 것이 가능하다. resource의 mapper폴더 안의 s2폴더 안에 저장한 MemberMapper.xml파일에 튜닝한 sql문을 작성해놓는다. 단건 조회, 다건 조회와 같이 목적에 맞는 CRUD 작성한다. 그리고 sql문에 ?값으로 대응되는 것으로 #{이름}이 있는데 #{}로 선언하면 preparedStatement로 선언하는 것이라서 왠만하면 #{}을 사용하도록 하자. <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="mapper.s2.MemberMapper"> <select id = "findAll" resultType="domain.Member"> SELECT id, name, password, email, balance FROM member </select> <select id = "findById" resultType="domain.Member" parameterType="long"> SELECT id, name, password, email, balance FROM member WHERE id = #{id} </select> <select id = "findByEmail" resultType="domain.Member" parameterType="String"> SELECT id, name, password, email, balance FROM member WHERE email = #{email} </select> <insert id = "save" useGeneratedKeys="true" keyProperty="id"> INSERT INTO member (name, password, email, balance) VALUES (#{name},#{password},#{email},#{balance}) </insert> <update id = "updateBalance"> UPDATE member SET balance = #{amount} WHERE id = #{id} </update> <delete id = "deleteById" parameterType="long"> DELETE FROM member WHERE id = #{id} </delete> <!-- resultType 체크--> <select id = "count" resultType="Integer"> SELECT COUNT(*) FROM MEMBER </select> </mapper> mapper 패키지에 저장한 interface MemberMapper interface를 활용 mapper 패키지에 Member 테이블에 관해 MemberMapper을 만들어 interface를 활용한 프록시 객체를 만들 수가 있다. public interface MemberMapper { //CRUD //다건조회 List<Member> findAll(); //단건조회 Optional<Member> findById(Long id); Optional<Member> findByEmail(Long Email); int save(Member member); int updateBalance(@Param("id")Long id,@Param("amount") int amount); int deleteById(Long id); // 총 회원수 int count(); } mybatis를 적용한 member 구현 앞의 설정한 mapper와 domain에 입력한 테이블의 정보를 가져와 구현 public class MemberCrud { static void main() throws Exception { MyBatisSessionConnector connector = new HikariMyBatisConnector( "mapper.s2", "domain" ); try(SqlSession connection = connector.openSession(true)){ MemberMapper memberMapper = connection.getMapper(MemberMapper.class); Member member1 = new Member( null, "user_" + genRandomNumber(), "1234", "user%d@email.com".formatted(genRandomNumber()), genRandomNumber() ); Member member2 = new Member( null, "user_" + genRandomNumber(), "1234", "user%d@email.com".formatted(genRandomNumber()), genRandomNumber() ); memberMapper.save(member1); memberMapper.save(member2); System.out.println("member1 id = "+ member1.getId()); System.out.println("member1 id = "+ member2.getId()); } } } 앞서 작성한 interface를 통해 작성한 프록시 객체를 활용함과 Dependency injection을 통해 유지보수가 쉽도록 구현했다. try with resources문을 만들어 close문을 작성하지 않아도 sql 연결에 소모되는 자원을 안전하게 반납하도록 구현했다. 여기서 connector객체는 jdbc의 트랜잭션의 기능을 구현한다. 이때 만약 connector의 openSession(true)를 하지 않고 openSession()을 하면 기본값이 false로 되어있어 데이터베이스에 commit이 적용이 되지않아 insert, update, delete 작업은 반영이 되지 않는다. 예제에서 사용한 몇가지 디자인 패턴정리 디자인 패턴은 수많이 반복되는 설계 문제에 대해 검증된 해결방법으로 코드를 구조적으로 분리하고 확장성을 증가시켜 구현할 수 있고 협업 시 소통을 원활하게 할 수 있으므로 정리가 필요하다. Proxy 실제 객체 대신 대리 객체를 사용하여, 접근을 제어하거나 공통 기능을 추가한 패턴이다. Spring의 AOP(관점지향 프로그래밍)이나 트랜잭션 처리에 핵심적으로 사용이 된다. public interface MemberMapper { //CRUD //다건조회 List<Member> findAll(); //단건조회 Optional<Member> findById(Long id); Optional<Member> findByEmail(Long Email); int save(Member member); int updateBalance(@Param("id")Long id,@Param("amount") int amount); int deleteById(Long id); // 총 회원수 int count(); } 인터페이스로 생성을 해주고 try(SqlSession connection = connector.openSession(true)){ MemberMapper memberMapper = connection.getMapper(MemberMapper.class); Member member1 = new Member( null, "user_" + genRandomNumber(), "1234", "user%d@email.com".formatted(genRandomNumber()), genRandomNumber() ); Member member2 = new Member( null, "user_" + genRandomNumber(), "1234", "user%d@email.com".formatted(genRandomNumber()), genRandomNumber() ); memberMapper.save(member1); memberMapper.save(member2); } 위 코드처럼 인터페이스를 활용하여 객체에 부가기능을 더하여 구현 Factory 객체 생성 로직을 별도의 클래스 or 메서드에 위임하는 패턴이다. 이를 통해 클라이언트가 concrete class에 의존하지 않아도 되어 결합도를 낮추고 확장도를 높일 수가 있다. Payment payment = new CardPayment(); 이렇게 생성하면 코드가 concrete class에 의존을 하게 되어 새로운 타입을 추가하는 경우 코드의 변경이 필요하다. 오늘 공부한 코드에서는 다음과 같은 코드가 factory적 요소를 만족한다고 볼 수가 있다. SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(configuration); 객체 생성 로직을 SqlSessionFactoryBuilder에 위임하여 캡슐화를 하고 있으므로 Factory의 특징을 만족한다고 볼 수가 있다. Builder 객체 생성 과정이 복잡할 때, 가독성과 안정성을 높이기 위해 단계적으로 객체를 생성하는 패턴 아까 Factory에서 사용한 코드도 builder 패턴을 가지고 볼 수가 있는데 실제로는 xml의 설정을 parsing하고 configuration -> Environment -> DataSource-> TransactionFactiory 설정 -> Mapper 등록 -> SqlSessionFactory 설정이라는 엄청 긴 과정을 한 줄로 캡슐화 과정을 거친 코드를 생성하여 복잡성을 낮추고 가독성을 높였다고 볼 수가 있다. SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(configuration);
Today I Learned
· 2026-04-20
<
>
Touch background to close